package com.inspectortime.service;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.StringUtils;

/**
 * An implementation that goes to a geonames.org web service to retrieve zip
 * code data, then parses it using a SAX parser.
 * 
 * @author mKvalsvik
 * 
 */
public class SaxZipCodeSearchServiceImpl extends AbstractZipCodeSearchService {

	protected static final String FIND_NEARBY_POSTAL_CODES_WEB_SERVICE_URL = "http://ws.geonames.org/findNearbyPostalCodes";
	protected static final float KM_PER_US_MILE = 1.609f;
	private static Log log = LogFactory
			.getLog(SaxZipCodeSearchServiceImpl.class);
	protected static final int MAX_ROWS = 500;
	protected static final String POSTAL_CODE_SEARCH_WEB_SERVICE_URL = "http://ws.geonames.org/postalCodeSearch";
	HttpClient client;

	MultiThreadedHttpConnectionManager connectionManager;

	private SaxCodeElementHandler handler;

	private SAXParser parser;

	protected Date wsEndTime;

	protected Date wsStartTime;

	public SaxZipCodeSearchServiceImpl() {
		handler = new SaxCodeElementHandler();
		SAXParserFactory factory = SAXParserFactory.newInstance();
		try {
			parser = factory.newSAXParser();
		} catch (Exception e) {
			throw new RuntimeException("Could not create SAX parser", e);
		}

		connectionManager = new MultiThreadedHttpConnectionManager();
		connectionManager.getParams().setDefaultMaxConnectionsPerHost(10);
		client = new HttpClient(connectionManager);
	}

	private String buildLookupZipCodeUrl(String zipCode) {
		// http://ws.geonames.org/postalCodeSearch?postalcode=30012&maxRows=10&
		// country=US
		String url = POSTAL_CODE_SEARCH_WEB_SERVICE_URL + "?postalcode="
				+ zipCode + "&country=US";
		log.debug("Starting web service call: " + url);
		return url;
	}

	private String buildNearbyZipCodesUrl(String zipCode, int radiusInMiles) {
		//http://ws.geonames.org/findNearbyPostalCodes?postalcode=30341&country=
		// US&radius=80&maxRows=500
		float radiusInKm = radiusInMiles * KM_PER_US_MILE;
		String url = FIND_NEARBY_POSTAL_CODES_WEB_SERVICE_URL + "?postalcode="
				+ zipCode + "&country=US&radius=" + radiusInKm + "&maxRows="
				+ MAX_ROWS;

		log.debug("Starting web service call: " + url);
		return url;
	}

	public double findDistanceInMiles(String zipCodeA, String zipCodeB) {
		ZipCodeInfo zipA = this.lookupZipCode(zipCodeA);
		ZipCodeInfo zipB = this.lookupZipCode(zipCodeB);
		if (zipA == null || zipB == null) {
			return -1;
		}
		return zipA.distanceTo(zipB);
	}

	public List<ZipCodeInfo> findNearbyZipCodes(String zipCode,
			int radiusInMiles) {

		String url = buildNearbyZipCodesUrl(zipCode, radiusInMiles);
		HttpMethod method = new GetMethod(url);
		try {
			wsStartTime = new Date();
			try {
				client.executeMethod(method);
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
			parse(method.getResponseBodyAsStream());
		} catch (IOException e) {
			throw new RuntimeException("Could not call web service: " + url, e);
		} finally {
			method.releaseConnection();
			wsEndTime = new Date();
		}
		List<ZipCodeInfo> results = null;
		if (handler.getElementNameValueMaps().isEmpty()) {
			results = Collections.EMPTY_LIST;
		} else {
			results = this.mapsToZipCodeInfos(handler
					.getElementNameValueMaps());	
		}		
		logStats(results.size(), radiusInMiles, zipCode);
		return results;
	}
	
	protected void logStats(int resultCount, int radiusInMiles, String zipCode) {
		Date parseEndTime = new Date();
		long duration = wsEndTime.getTime() - wsStartTime.getTime();
		long parseTime = parseEndTime.getTime() - wsEndTime.getTime();
		log.debug(FIND_NEARBY_POSTAL_CODES_WEB_SERVICE_URL + ": Got "
				+ resultCount + " zip codes within " + radiusInMiles
				+ " miles of " + zipCode + " in " + duration + "ms, parsed in "
				+ parseTime + "ms.");

	}

	public ZipCodeInfo lookupZipCode(String zipCode) {
		String url = buildLookupZipCodeUrl(zipCode);
		HttpMethod method = new GetMethod(url);
		try {
			wsStartTime = new Date();
			try {
				client.executeMethod(method);
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
			parse(method.getResponseBodyAsStream());
		} catch (IOException e) {
			throw new RuntimeException("Could not call web service: " + url, e);
		} finally {
			method.releaseConnection();
			wsEndTime = new Date();
		}
		if (handler.getElementNameValueMaps().isEmpty()) {
			return null;
		}
		ZipCodeInfo info = this.mapToZipCodeInfo(handler
				.getFirstElementNameValue());
		logStats(handler.getElementNameValueMaps().size(), -1, zipCode);
		return info;

	}

	private List<ZipCodeInfo> mapsToZipCodeInfos(List<Map<String, String>> maps) {
		List<ZipCodeInfo> results = new ArrayList<ZipCodeInfo>();
		List<Map<String, String>> list = handler.getElementNameValueMaps();
		for (Map<String, String> map : list) {
			results.add(this.mapToZipCodeInfo(map));
		}
		return results;
	}

	private ZipCodeInfo mapToZipCodeInfo(Map<String, String> nameValuePairs) {
		ZipCodeInfo info = new ZipCodeInfo();
		info.setCity(nameValuePairs.get("name"));
		info.setCountyCode(nameValuePairs.get("adminCode2"));
		info.setCountyName(nameValuePairs.get("adminName2"));
		String distanceStr = nameValuePairs.get("distance");
		if (StringUtils.hasText(distanceStr)) {
			info.setOptionalDistanceToSource(Float.valueOf(distanceStr));
		}
		info.setLatitude(Float.valueOf(nameValuePairs.get("lat")));
		info.setLongitude(Float.valueOf(nameValuePairs.get("lng")));
		info.setStateCode(nameValuePairs.get("adminCode1"));
		info.setStateName(nameValuePairs.get("adminName1"));
		info.setZipCode(nameValuePairs.get("postalcode"));
		return info;
	}

	private void parse(InputStream in) {
		try {
			parser.parse(in, handler);
		} catch (Exception e) {
			throw new RuntimeException("Could not parse web service results", e);
		}
	}
	
}
